//
// Created by Administrator on 2022/1/18.
//

#include "glad/glad.h"
#include "GLFW/glfw3.h"
#include "Common/Utils.h"
#include "Common/Torus.h"
#include "glm/glm.hpp"
#include "glm/gtc/type_ptr.hpp"

// ShadingModel:
// 1. flat shading model
// 2. phong shading model
// 3. binn-phong shading model
#define SHADING_MODEL 3

#define numVAOs 1
#define numVBOs 4
GLuint vao[numVAOs];
GLuint vbo[numVBOs];
GLuint texture;
GLuint renderingProgram;
glm::vec3 cameraPos;
glm::mat4 projMatrix, viewMatrix, modelMatrix;

Torus torus(0.5f, 0.2f, 48);

void Init(GLFWwindow* window);
void Update(GLFWwindow* window, double currentTime);
void Render();
void DeInit();

void OnWindowSizeCallback(GLFWwindow* window, int w, int h);
void SetupVertices();
void SetLightUniforms();
void SetMaterialUniforms();

int main()
{
    if(!glfwInit())
    {
        return 1;
    }
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    GLFWwindow* window = glfwCreateWindow(800, 600, "Chapter7", nullptr, nullptr);
    if(!window)
    {
        glfwTerminate();
        return 1;
    }
    glfwMakeContextCurrent(window);
    glfwSetWindowSizeCallback(window, OnWindowSizeCallback);

    if(!gladLoadGLLoader(GLADloadproc(glfwGetProcAddress)))
    {
        glfwDestroyWindow(window);
        glfwTerminate();
        return 1;
    }

    Init(window);
    glfwSwapInterval(1);
    while(!glfwWindowShouldClose(window))
    {
        Update(window, glfwGetTime());
        Render();
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    DeInit();

    glfwDestroyWindow(window);
    glfwTerminate();

    return 0;
}

void Init(GLFWwindow* window)
{
    if(SHADING_MODEL == 1)
    {
        renderingProgram = Utils::CreateShaderProgram("../../Chapter7/flatLitVert.glsl",
                                                      "../../Chapter7/flatLitFrag.glsl");
    }
    else if(SHADING_MODEL == 2)
    {
        renderingProgram = Utils::CreateShaderProgram("../../Chapter7/PhongLitVert.glsl",
                                                      "../../Chapter7/PhongLitFrag.glsl");
    }
    else if(SHADING_MODEL == 3)
    {
        renderingProgram = Utils::CreateShaderProgram("../../Chapter7/Blinn-PhongLitVert.glsl",
                                                      "../../Chapter7/Blinn-PhongLitFrag.glsl");
    }

    texture = Utils::LoadTexture("../../Chapter7/wall.jpg");

    cameraPos = glm::vec3(0.0f, 0.0f, 8.0f);
    SetupVertices();
    int w, h;
    glfwGetFramebufferSize(window, &w, &h);
    projMatrix = glm::perspective(Utils::Deg2Rad(60.0f), float(w) / float(h), 0.1f, 1000.0f);
}
void Update(GLFWwindow* window, double currentTime)
{
    viewMatrix = glm::translate(glm::mat4(1.0f), -cameraPos);
    modelMatrix = glm::rotate(glm::mat4(1.0f), float(currentTime), glm::vec3(0.0f, 1.0f, 0.0f));
    modelMatrix = glm::rotate(modelMatrix, Utils::Deg2Rad(30.0f), glm::vec3(1.0f, 0.0f, 0.0f));
    modelMatrix = glm::scale(modelMatrix, glm::vec3(5.0f));
}
void Render()
{
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glUseProgram(renderingProgram);
    GLint vpMatrixLoc = glGetUniformLocation(renderingProgram, "vpMatrix");
    GLint objectToWorldMatrixLoc = glGetUniformLocation(renderingProgram, "objectToWorldMatrix");
    GLint worldToObjectMatrixLoc = glGetUniformLocation(renderingProgram, "worldToObjectMatrix");
    GLint cameraPosLoc = glGetUniformLocation(renderingProgram, "cameraPos");

    glUniformMatrix4fv(vpMatrixLoc, 1, GL_FALSE, glm::value_ptr(projMatrix * viewMatrix));
    glUniformMatrix4fv(objectToWorldMatrixLoc, 1, GL_FALSE, glm::value_ptr(modelMatrix));
    glUniformMatrix4fv(worldToObjectMatrixLoc, 1, GL_FALSE, glm::value_ptr(glm::inverse(modelMatrix)));
    glUniform3fv(cameraPosLoc, 1, glm::value_ptr(cameraPos));

    SetLightUniforms();
    SetMaterialUniforms();

    glBindVertexArray(vao[0]);

    // configure
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);
    // draw
    glDrawElements(GL_TRIANGLES, torus.getNumIndices(), GL_UNSIGNED_INT, nullptr);
}
void DeInit()
{
    glDeleteVertexArrays(numVAOs, vao);
    glDeleteBuffers(numVBOs, vbo);
    glDeleteTextures(1, &texture);
    glDeleteProgram(renderingProgram);
}

void OnWindowSizeCallback(GLFWwindow* window, int w, int h)
{
    glViewport(0, 0, w, h);
    projMatrix = glm::perspective(Utils::Deg2Rad(60.0f), float(w) / float(h), 0.1f, 1000.0f);
}
void SetupVertices()
{
    int numVertices = torus.getNumVertices();
    int numIndices = torus.getNumIndices();
    glGenVertexArrays(numVAOs, vao);
    glBindVertexArray(vao[0]);
    glGenBuffers(numVBOs, vbo);

    glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
    glBufferData(GL_ARRAY_BUFFER, numVertices * 3 * sizeof(float), torus.getVertices().data(), GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
    glBufferData(GL_ARRAY_BUFFER, numVertices * 2 * sizeof(float), torus.getUVs().data(), GL_STATIC_DRAW);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
    glEnableVertexAttribArray(1);

    glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);
    glBufferData(GL_ARRAY_BUFFER, numVertices * 3 * sizeof(float), torus.getNormals().data(), GL_STATIC_DRAW);
    glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
    glEnableVertexAttribArray(2);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[3]);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, numIndices * sizeof(int), torus.getIndices().data(), GL_STATIC_DRAW);
}

void SetLightUniforms()
{
    // global
    glUniform4fv(glGetUniformLocation(renderingProgram, "global.ambient"), 1,
                 glm::value_ptr(glm::vec4(0.2f, 0.2f, 0.2f, 1.0f)));
    // main light
    glUniform4fv(glGetUniformLocation(renderingProgram, "mainLight.ambient"), 1,
                 glm::value_ptr(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)));
    glUniform4fv(glGetUniformLocation(renderingProgram, "mainLight.diffuse"), 1,
                 glm::value_ptr(glm::vec4(1.0f, 0.9568f, 0.8392f, 1.0f)));
    glUniform4fv(glGetUniformLocation(renderingProgram, "mainLight.specular"), 1,
                 glm::value_ptr(glm::vec4(1.0f, 1.0f, 1.0f, 1.0f)));
    glUniform3fv(glGetUniformLocation(renderingProgram, "mainLight.direction"), 1,
                 glm::value_ptr(glm::normalize(glm::vec3(-6.0f, -6.0f, -6.0f))));

    // additional light
    constexpr GLsizei count = 2;
    float* ambients[count];
    float* diffuses[count];
    float* speculars[count];
    float* positions[count];
    float range[count];
    float* direction[count];
    float rangeMin[count];
    float innerCos[count];
    float outerCos[count];
    for(int i = 0; i < count; ++i)
    {
        ambients[i] = const_cast<float*>(glm::value_ptr(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)));
        diffuses[i] = const_cast<float*>(glm::value_ptr(
                i == 0 ?
                glm::vec4(1.0f, 0.0f, 0.0f, 1.0f) :
                glm::vec4(0.0f, 1.0f, 0.0f, 1.0f)));
        speculars[i] = const_cast<float*>(glm::value_ptr(glm::vec4(1.0f, 1.0f, 1.0f, 1.0f)));
        positions[i] = const_cast<float*>(glm::value_ptr(
                i == 0 ?
                glm::vec3(0.0f, 0.0f, 10.0f) :
                glm::vec3(-5.0f, 5.0f, 5.0f)));
        range[i] = 15.0f;
        direction[i] = const_cast<float*>(glm::value_ptr(
                i == 0 ?
                glm::vec3(0.0f, 0.0f, -1.0f) :
                glm::vec3(0.0f)));
        rangeMin[i] = 5.0f;
        innerCos[i] = i == 0 ? cos(Utils::Deg2Rad(5.0f)) : -1.0f;
        outerCos[i] = i == 0 ? cos(Utils::Deg2Rad(10.0f)) : -1.0f;
    }
    glUniform1ui(glGetUniformLocation(renderingProgram, "additionalLightCount"), count);
    glUniform4fv(glGetUniformLocation(renderingProgram, "additionalLights.ambient"), count,
                 ambients[0]);
    glUniform4fv(glGetUniformLocation(renderingProgram, "additionalLights.diffuse"), count,
                 diffuses[0]);
    glUniform4fv(glGetUniformLocation(renderingProgram, "additionalLights.specular"), count,
                 speculars[0]);
    glUniform3fv(glGetUniformLocation(renderingProgram, "additionalLights.position"), count,
                 positions[0]);
    glUniform1fv(glGetUniformLocation(renderingProgram, "additionalLights.range"), count,
                range);
    glUniform3fv(glGetUniformLocation(renderingProgram, "additionalLights.direction"), count,
                 direction[0]);
    glUniform1fv(glGetUniformLocation(renderingProgram, "additionalLights.rangeMin"), count,
                 rangeMin);
    glUniform1fv(glGetUniformLocation(renderingProgram, "additionalLights.innerCos"), count,
                 innerCos);
    glUniform1fv(glGetUniformLocation(renderingProgram, "additionalLights.outerCos"), count,
                 outerCos);
}
void SetMaterialUniforms()
{
    glUniform4fv(glGetUniformLocation(renderingProgram, "material.ambient"), 1,
                 Utils::GoldAmbient());
    glUniform4fv(glGetUniformLocation(renderingProgram, "material.diffuse"), 1,
                 Utils::GoldDiffuse());
    glUniform4fv(glGetUniformLocation(renderingProgram, "material.specular"), 1,
                 Utils::GoldSpecular());
    glUniform1f(glGetUniformLocation(renderingProgram, "material.shininess"),
                 Utils::GoldShininess());
}